/*
 *  Axolotl - Alternate persistence for Grails
 *  Copyright (C) 2008  Juanjo García Latre
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.googlecode.axolotl.jdbc;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.commons.GrailsDomainClass;
import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import com.googlecode.axolotl.lazy.LazyEntity;
import com.googlecode.axolotl.query.And;
import com.googlecode.axolotl.query.Between;
import com.googlecode.axolotl.query.Conjunction;
import com.googlecode.axolotl.query.Criterion;
import com.googlecode.axolotl.query.Equals;
import com.googlecode.axolotl.query.GreaterOrEqual;
import com.googlecode.axolotl.query.In;
import com.googlecode.axolotl.query.LessOrEqual;
import com.googlecode.axolotl.query.Like;
import com.googlecode.axolotl.query.Or;
import com.googlecode.axolotl.query.Order;
import com.googlecode.axolotl.query.PropertyName;
import com.googlecode.axolotl.query.Query;
import com.googlecode.axolotl.query.QueryVisitor;


/**
 * 
 *
 * @author juanjo
 */
public class GrailsDomainSqlCriteriaBuilder implements QueryVisitor<String> {

	private GrailsApplication application;
	private GrailsDomainClass domainClass;
	private SqlFactory sqlFactory;
	private List<JoinInfo> joins = new ArrayList<JoinInfo>();
	private List<String> orders = new ArrayList<String>();
	private boolean distinct = false;


	/**
	 *
	 */
	public GrailsDomainSqlCriteriaBuilder(GrailsApplication application, GrailsDomainClass domainClass, SqlFactory sqlFactory) {
		this.application = application;
		this.domainClass = domainClass;
		this.sqlFactory = sqlFactory;
	}


	/**
	 *
	 */
	public Collection<JoinInfo> getJoins() {
		return joins;
	}


	/**
	 * 
	 */
	public Collection<String> getOrders() {
		return orders;
	}


	/**
	 * 
	 */
	public boolean needsDistinct() {
		return distinct;
	}

	/*-------------------------------------------------------------------------*/

	/**
	 *
	 */
	public String visit(PropertyName propertyName) {
		String sql = "";      

		if (propertyName.getParent() == null) {
			/* Orphan property. If we came from recursive calls we're over.
			 */
			GrailsDomainClassProperty p = resolve(propertyName);

			if (p.isPersistent()) {  //getPersistentName() != null) {
				Class clazz = domainClass.getClazz();
				sql = sqlFactory.getTableName(clazz) + '.' + sqlFactory.getColumnName(clazz, p.getName());
			}
		}
		else {
			/* Nested property, i.e. it has a parent. 
			 */
			PropertyName parentName = propertyName.getParent();
			GrailsDomainClassProperty parentProp = resolve(parentName);

			if (parentProp.isManyToOne() || parentProp.isOneToOne()) {
				sql = renderNestedReference(propertyName, parentName, parentProp);
			}
			else if (parentProp.isOneToMany()) {
				sql = renderNestedList(propertyName, parentName, parentProp);
			}
		}
		return sql;
	}


	/**
	 *
	 */
	public String visit(And and) {
		return render(and, "and");
	}


	/**
	 *
	 */
	public String visit(Or or) {
		return render(or, "or");
	}


	/**
	 *
	 */
	public String visit(Equals equals) {
		PropertyName propertyName = equals.getProperty();
		String sqlColumn = propertyName.accept(this);
		CharSequence sqlValue = toSqlValue(resolve(propertyName), equals.getValue());
//		GrailsDomainClassProperty property = resolve(propertyName);
		
		return new StringBuilder()
				.append(sqlColumn)
				.append(equals.isNegated() ? " != " : " = ")
				.append(sqlValue)
				.toString();
//		ColumnType type = sqlFactory.getColumnType( property );
//		int precision = sqlFactory.getColumnPrecision( property );
//
//		return type.renderEquals(equ.isNegated(), sqlColumn, equ.getValue(), precision);
	}


	/**
	 * 
	 */
	public String visit(GreaterOrEqual ge) {
		PropertyName prp = ge.getProperty();
		String sqlColumn = prp.accept(this);
		CharSequence sqlValue = toSqlValue(resolve(prp), ge.getValue());
		
		return new StringBuilder()
				.append(sqlColumn)
				.append(ge.isNegated() ? " < " : " >= ")
				.append(sqlValue)
				.toString();		
//		GrailsDomainClassProperty property = resolve(prp);
//		ColumnType type = sqlFactory.getColumnType( property );
//		int precision = sqlFactory.getColumnPrecision( property );
//
//		return type.renderGreaterOrEqual(ge.isNegated(), sqlColumn, ge.getValue(), precision);
	}


	/**
	 * 
	 */
	public String visit(LessOrEqual le) {
		PropertyName prp = le.getProperty();
		String sqlColumn = prp.accept(this);
		CharSequence sqlValue = toSqlValue(resolve(prp), le.getValue());
		
		return new StringBuilder()
				.append(sqlColumn)
				.append(le.isNegated() ? " > " : " <= ")
				.append(sqlValue)
				.toString();		
//		GrailsDomainClassProperty property = resolve(prp);
//		ColumnType type = sqlFactory.getColumnType( property );
//		int precision = sqlFactory.getColumnPrecision( property );
//
//		return type.renderLessOrEqual(le.isNegated(), sqlColumn, le.getValue(), precision);
	}


	/**
	 *
	 */
	public String visit(Between btw) {
		PropertyName prp = btw.getProperty();
		String sqlColumn = prp.accept(this);
		GrailsDomainClassProperty property = resolve(prp);
		CharSequence sqlFrom = toSqlValue(property, btw.getFromValue());
		CharSequence sqlTo = toSqlValue(property, btw.getToValue());
		
		StringBuilder builder = new StringBuilder();
		builder.append(sqlColumn)
				.append(" between ")
				.append(sqlFrom)
				.append(" and ")
				.append(sqlTo);
		if (btw.isNegated()) {
			builder.insert(0, "not (").append(")");
		}
		return builder.toString();

//		GrailsDomainClassProperty property = resolve(prp);
//		ColumnType type = sqlFactory.getColumnType( property );
//		int precision = sqlFactory.getColumnPrecision( property );
//
//		return type.renderBetween(btw.isNegated(), sqlColumn, btw.getFromValue(), btw.getToValue(), precision);     
	}


	/**
	 *
	 */
	public String visit(Like like) {
		PropertyName prp = like.getProperty();
		String sqlColumn = prp.accept(this);
		CharSequence sqlValue = toSqlValue(resolve(prp), like.getValue());
		
		StringBuilder builder = new StringBuilder();
		builder.append(sqlColumn)
				.append(" like ")
				.append(sqlValue);
		if (like.isNegated()) {
			builder.insert(0, "not (").append(")");
		}
		return builder.toString();

//		GrailsDomainClassProperty property = resolve(prp);
//		ColumnType type = sqlFactory.getColumnType( property );
//		int precision = sqlFactory.getColumnPrecision( property );
//
//		return type.renderLike(lk.isNegated(), sqlColumn, lk.getPattern(), precision);
	}


	/**
	 * 
	 */
	public String visit(In in) {
		PropertyName prp = in.getProperty();
		String sqlColumn = prp.accept(this);
		GrailsDomainClassProperty property = resolve(prp);
		
		StringBuilder builder = new StringBuilder();
		builder.append(sqlColumn).append(" in (");

		for (Iterator i = ((Iterable) in.getValue()).iterator(); i.hasNext();) {
			builder.append( toSqlValue(property, i.next()) );
			if (i.hasNext()) {
				builder.append(',');
			}
		}
		builder.append(')');

		if (in.isNegated()) {
			builder.insert(0, "not (").append(")");
		}
		return builder.toString();

//		ColumnType type = sqlFactory.getColumnType( property );
//		int precision = sqlFactory.getColumnPrecision( property );
//
//		return type.renderIn(in.isNegated(), sqlColumn, (Iterable) in.getValue(), precision);
	}


	/**
	 *
	 */
	public String visit(Order order) {
		PropertyName propertyName = order.getProperty();
		String sqlColumn = propertyName.accept(this);
//		ColumnType type = sqlFactory.getColumnType( resolve(propertyName) );

		/* registramos las expresiones de los órdenes porque más tarde
		 * pueden hacer falta en el select, en caso que sea tipo distinct.
		 */
		orders.add(sqlColumn);      

		StringBuilder builder = new StringBuilder(sqlColumn);
		if (order.isAscendant()) {
			builder.append(" asc");
		}
		if (order.getNext() != null) {
			builder.append(", ").append( order.getNext().accept(this) );
		}
		return builder.toString();
	}


	public String visit(Query q) {
		// TODO Auto-generated method stub
		return null;
	}


	/*-------------------------------------------------------------------------*/

	protected CharSequence toSqlValue(GrailsDomainClassProperty property, Object value) {
		Class type = property.getType();
		
		if (Number.class.isAssignableFrom(type)) {
			return String.valueOf(value);
		}
		else if (CharSequence.class.isAssignableFrom(type)) {
			return new StringBuilder().append('\'').append(value).append('\'');
		}
		else if (Date.class.isAssignableFrom(type)) {
			// TODO
		}
		else if (Boolean.class.isAssignableFrom(type)) {
			return Boolean.valueOf( String.valueOf(value) ) ? "true" : "false";
		}
		else if (property.isAssociation()) {
			GrailsDomainClassProperty idProperty = property.getReferencedDomainClass().getIdentifier();
			Object idValue;
			if (LazyEntity.isLazy(value)) {
				idValue = LazyEntity.getId(value);
			}
			else {
				BeanWrapper wrapper = (value instanceof BeanWrapper) ? (BeanWrapper) value : new BeanWrapperImpl(value);
				idValue = wrapper.getPropertyValue(idProperty.getName());
			}
			return toSqlValue(idProperty, idValue);
		}
		System.out.println("unknown column type " + type);
		return null;
	}

		
	private GrailsDomainClassProperty resolve(PropertyName propertyName) {
		return resolve( domainClass, propertyName.getCanonicalName() );
	}

	
	private GrailsDomainClassProperty resolve(GrailsDomainClass domainClass, String propertyPath) {
		final int pos = propertyPath.indexOf(PropertyName.NAME_SEPARATOR);
		if (pos == -1) {
			return domainClass.getPropertyByName(propertyPath);
		}
		else {
			String first = propertyPath.substring(0, pos);
			GrailsDomainClassProperty property = domainClass.getPropertyByName(first);
			GrailsDomainClass refClass = property.getReferencedDomainClass();
			
			String last = propertyPath.substring(pos + 1);
			return resolve(refClass, last);
		}
	}

	
	/**
	 *
	 */
	protected String render(Conjunction conj, String oper) {
		StringBuilder sql = new StringBuilder();

		if (conj.getCriteria().size() > 0) {
			if (conj.isNegated()) {
				sql.append("not ");
			}
			sql.append('(');

			Iterator<Criterion> ite = conj.getCriteria().iterator();
			boolean lastRendered = false;

			while (ite.hasNext()) {
				String c = ite.next().accept(this);

				if (c != null && c.length() > 0) {
					if (lastRendered) {
						sql.append(' ').append(oper).append(' ');
					}
					sql.append(c);
					lastRendered = true;
				}
				else {
					lastRendered = false;
				}
			}
			sql.append(')');
		}

		return sql.toString();
	}


	/**
	 * Registra una tabla y dos columnas para hacer un JOIN. 
	 */
	private void registerJoin(GrailsDomainClass domainClass, String column1, String column2) {
		String table = sqlFactory.getTableName(domainClass.getClazz());
		String criterion = new StringBuilder(column1)
					.append(" = ").append(column2).toString(); 

		JoinInfo joinInfo = new JoinInfo(table, criterion);

		if (!joins.contains(joinInfo)) {
			joins.add(joinInfo);		
		}
	}


	/**
	 *
	 *
	 */
	public static class JoinInfo {
		private String table;
		private String criterion;

		public JoinInfo(String table, String criterion)	{
			this.table = table;
			this.criterion = criterion;
		}

		public String getTable() {
			return table;
		}

		public String getCriterion() {
			return criterion;
		}

		public boolean equals(Object o) {
			return o instanceof JoinInfo &&
					((JoinInfo) o).table.equals(this.table) &&
					((JoinInfo) o).criterion.equals(this.criterion);
		}
	}


	/**
	 * 
	 */
	private String renderNestedReference(PropertyName childName, PropertyName parentName, 
			GrailsDomainClassProperty parentProp) {
		
		/* la propiedad padre es de tipo entidad, por lo tanto habra que hacer
		 * un join con la tabla correspondiente al tipo de la propidedad padre.
		 * Obtenemos la metainformacion para esa tabla.
		 */
		Class parentType = parentName.getNarrowType() != null ?
				parentName.getNarrowType() :
				parentProp.getReferencedPropertyType();
		GrailsDomainClass joinClass = 
			(GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, parentType.getName());

		/* obtenemos un renderizador que resuelva los nombres de propiedad
		 * en el contexto de la tabla con la que hacemos el join. 
		 */
		GrailsDomainSqlCriteriaBuilder joinBuilder = 
				new GrailsDomainSqlCriteriaBuilder(application, joinClass, sqlFactory);

		/* renderizamos la columna oid de la tabla con la que hacemos el join. 
		 */
		PropertyName oidName = new PropertyName( joinClass.getIdentifier().getName() ); 
		String joinColumn = oidName.accept(joinBuilder);

		/* renderizamos la columna correspondiente a la propiedad padre
		 * en el contexto de este Mapping. Esto provocará recursividad
		 * si la propiedad padre es anidada. 
		 */
		String parentColumn = parentName.accept(this);

		/* registramos la información para renderizar el join más tarde.
		 */
		registerJoin(joinClass, parentColumn, joinColumn);

		/* creamos una copia huérfana de la propiedad original y la renderizamos
		 * en el contexto del mapping del padre. El resultado será lo que
		 * devolverá este método.
		 */
		PropertyName leafName = new PropertyName(childName.getName()); 
		String result = leafName.accept(joinBuilder);

		return result;
	}


	/**
	 * 
	 */
	private String renderNestedList(PropertyName childName, PropertyName parentName, GrailsDomainClassProperty parentProp) {
		this.distinct = true;

		/* Renderizamos la columna oid de la tabla a la que pertenece
		 * la propiedad padre. Tenemos en cuenta que este padre a su
		 * vez puede tener otro, lo que provocará recursividad.
		 * @todo ¿el nombre del oid se está cogiendo del mapping correcto?
		 */
		PropertyName oidName = new PropertyName(parentName.getParent(), domainClass.getIdentifier().getName());//mapping.getOidProperty().getName());
		String oneColumn = oidName.accept(this);

		/* Renderizamos la columna que hace de clave foránea en la tabla
		 * correspondiente a la lista.
		 */   
		GrailsDomainClassProperty fkProperty = parentProp.getOtherSide();
		GrailsDomainClass manyClass = parentName.getNarrowType() != null ?
				(GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, parentName.getNarrowType().getName()) :
				fkProperty.getDomainClass();
		GrailsDomainSqlCriteriaBuilder manyRenderer = new GrailsDomainSqlCriteriaBuilder(application, manyClass, sqlFactory);
		PropertyName fkName = new PropertyName(fkProperty.getName());
		String manyColumn = fkName.accept(manyRenderer);

		/* 
		 */   
		registerJoin(manyClass, oneColumn, manyColumn);

		/* 
		 */   
		PropertyName leafName = new PropertyName(childName.getName());
		String result = leafName.accept(manyRenderer);

		return result;
	}

}
